package org.sqlproc.engine.impl;

import java.util.HashMap;
import java.util.Map;

import org.antlr.runtime.ANTLRStringStream;
import org.antlr.runtime.CommonTokenStream;
import org.antlr.runtime.RecognitionException;
import org.hibernate.SQLQuery;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.sqlproc.engine.SqlEngineException;

/**
 * The precompiled Mapping entity for one META SQL statement.
 * 
 * Instances of this class are created by the ANTLR parser. The grammar itself is defined in SqlMapping.g.
 * 
 * The main runtime contracts are {@link org.sqlproc.engine.impl.SqlMappingRule#setQueryResultData(Object, Object[])}
 * and {@link org.sqlproc.engine.impl.SqlMappingRule#setQueryResultMapping(Class, SQLQuery)}.
 * 
 * <p>
 * Schematically:
 * 
 * <pre>
 * SqlMappingRule = SqlMappingItem+
 * 
 *     SqlMappingItem = dbName.sqlType.javaName.javaName...
 * </pre>
 */
public class SqlMappingRule {

    /**
     * The internal slf4j logger.
     */
    protected static Logger logger = LoggerFactory.getLogger(SqlMappingRule.class);

    /**
     * All sub-elements based on ANTLR grammar defined in SqlMapping.g. Every sub-element is one Mapping item.
     */
    private Map<String, SqlMappingItem> mappings;

    /**
     * Simple factory method (design pattern). The new instance of precompiled Mapping rule is created from the String
     * input by the ANTLR parser.
     * 
     * @param mappingStatement
     *            String representation of Mapping rule
     * @return new instance of precompiled Mapping rule
     * @throws SqlEngineException
     *             in the case of ANTRL parsing exception
     */
    public static SqlMappingRule getInstance(String mappingStatement) throws SqlEngineException {
        if (logger.isDebugEnabled()) {
            logger.debug(">> getInstance, mappingStatement=" + mappingStatement);
        }
        SqlMappingRule mapping = null;
        try {
            StringBuilder s = new StringBuilder(mappingStatement);
            SqlMappingLexer lexer = new SqlMappingLexer(new ANTLRStringStream(s.toString()));
            CommonTokenStream tokens = new CommonTokenStream(lexer);
            SqlMappingParser parser = new SqlMappingParser(tokens);
            try {
                mapping = parser.parse();
            } catch (RecognitionException ex) {
                ex.printStackTrace();
            }
            if (!lexer.getErrors().isEmpty() || !parser.getErrors().isEmpty()) {
                throw new SqlEngineException("Mapping error for '" + mappingStatement + "'", lexer.getErrors(),
                        parser.getErrors());
            }
            return mapping;
        } finally {
            if (logger.isDebugEnabled()) {
                logger.debug("<< getInstance, mapping=" + mapping);
            }
        }
    }

    /**
     * Creates a new instance. It's used from inside ANTLR parser.
     */
    public SqlMappingRule() {
        mappings = new HashMap<String, SqlMappingItem>();
    }

    /**
     * Creates a new instance.
     * 
     * @param mappings
     *            sub-elements based on ANTLR grammar defined in SqlStatement.g
     */
    SqlMappingRule(Map<String, SqlMappingItem> mappings) {
        this.mappings = mappings;
    }

    /**
     * Adds a new Mapping item, which is a mapping rule for one column.
     * 
     * @param item
     *            the Mapping item
     */
    void addMapping(SqlMappingItem item) {
        mappings.put(item.getDbName(), item);
    }

    /**
     * Declares a scalar query result for all Mapping rule items.
     * 
     * @param resultClass
     *            The class used for the return values, the SQL execution output.
     * @param query
     *            Hibernate SQL Query instance
     */
    public void setQueryResultMapping(Class<?> resultClass, SQLQuery query) {
        for (SqlMappingItem item : mappings.values()) {
            item.setQueryResultMapping(resultClass, query);
        }
    }

    /**
     * Fills the instance of the result class with output values from the SQL query execution.
     * 
     * @param resultInstance
     *            The instance of the result class.
     * @param resultValues
     *            Query execution output values
     */
    public void setQueryResultData(Object resultInstance, Object[] resultValues) {
        int i = 0;
        for (SqlMappingItem item : mappings.values()) {
            item.setQueryResultData(resultInstance, resultValues[i++]);
        }
    }

    /**
     * Merge mapping rule for one META SQL query based on SqlMapping.g and SqlStatement.g. The mapping rule based on
     * SqlMapping.g has the higher priority compared to mapping rule based on SqlStatement.g. The mapping rule based on
     * SqlStatement.g obtains a list of real output values.
     * 
     * @param mapping
     *            the mapping rule based on SqlMapping.g
     * @param processResult
     *            obtains a mapping rule based on SqlStatement.g
     * @return a new merged mapping rule
     */
    public static SqlMappingRule merge(SqlMappingRule mapping, SqlProcessResult processResult) {
        if (mapping == null)
            mapping = new SqlMappingRule();
        return mapping.merge(processResult.getOutputValues());
    }

    /**
     * Merge mapping rule for one META SQL query based on SqlMapping.g and SqlStatement.g. This mapping rule has the
     * higher priority. The mapping rule based on SqlStatement.g obtains a list of real output values.
     * 
     * @param outputMappings
     *            mapping rule based on SqlStatement.g
     * @return a new merged mapping rule
     */
    SqlMappingRule merge(Map<String, SqlMappingItem> outputMappings) {
        if (outputMappings == null || outputMappings.size() == 0)
            return this;

        SqlMappingRule resultMapping = new SqlMappingRule();
        for (SqlMappingItem mappingItem : outputMappings.values()) {
            if (mappings.containsKey(mappingItem.getDbName())) {
                resultMapping.addMapping(mappings.get(mappingItem.getDbName()).merge(mappingItem));
            } else {
                resultMapping.addMapping(mappingItem);
            }
        }

        return resultMapping;
    }
}
